package de.jmda.mview;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.imageio.ImageIO;

import javax.xml.bind.Unmarshaller;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.swing.JDesktopPane;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;

import org.apache.log4j.Logger;

import de.jmda.mproc.ProcessingUtilities;
import de.jmda.mproc.RelationInfoCalculator;
import de.jmda.mproc.TypeElementDictionarySingleton;
import de.jmda.mproc.RelationInfoCalculator.RelationInfoHas;
import de.jmda.mview.typeshape.DisplayOptions;
import de.jmda.mview.typeshape.DisplayOptionsModelContainer;
import de.jmda.mview.typeshape.TypeShape;
import de.jmda.mview.typeshape.relation.Relation;
import de.jmda.mview.typeshape.relation.RelationHasA;
import de.jmda.mview.typeshape.relation.RelationIsA;

/**
 * Instances may contain {@link Typeshape}s and {@link Relation}s. <code>
 * TypeShape</code> operations delegate to the super class to let it provide
 * default behaviour and <code>Relation</code> operations are handled
 * individually in this class.
 * <p>
 * <code>Relation</code> instances can not be added nor removed directly from
 * outside this container. <code>Relation</code> instances are computed by this
 * class.
 *
 * @author roger.jmda@gmail.com
 */
public class DesktopPane
		extends JDesktopPane
		implements JAXBModelProvider<DesktopPane.JAXBModel>
{
	private static final long serialVersionUID = 1L;
	
	private static final Logger LOGGER = Logger.getLogger(DesktopPane.class);

	private static final String DEFAULT_NAME = "default diagram";
	
	@XmlRootElement(name="DesktopPane.Model")
	@XmlAccessorType(XmlAccessType.FIELD)
	public static class JAXBModel extends JAXBModelNamed
	{
		@XmlTransient
		private static final Logger LOGGER = Logger.getLogger(JAXBModel.class);

		@XmlElement
		public DisplayOptionsModelContainer displayOptionsModelContainer;

		@XmlElementWrapper(name="typeshapes")
	  @XmlElements(@XmlElement(name="typeshape"))
		public Set<TypeShape.JAXBModel> typeshapeJAXBModels;
		
		@XmlTransient
		private DesktopPane desktopPane;

//		@XmlElementWrapper
//	  @XmlElements(@XmlElement(name="relation"))
//		Set<Relation.RelationXMLModel> relationXMLModels;
		
		/**
		 * make JAXB happy
		 * <p>
		 * preserve this constructor for JAXB, do not call other constructors with
		 * default values
		 */
		private JAXBModel()
    {
			super();
    }
		
		/**
		 * @param desktopPane
		 */
		JAXBModel(DesktopPane desktopPane)
		{
			this();
			
			this.desktopPane = desktopPane;
		}

		/**
		 * JAXB marshalling callback method
		 *
		 * @param marshaller
		 */
		void beforeMarshal(Marshaller marshaller)
		{
			if (desktopPane == null)
			{
				LOGGER.debug("desktop pane not yet initialised");
				return;
			}

			LOGGER.debug("updating jaxb model from desktop pane");
			
			name = desktopPane.name;

			displayOptionsModelContainer = desktopPane.displayOptionsModelContainer;

			typeshapeJAXBModels = new HashSet<TypeShape.JAXBModel>();
			
			for (TypeShape<?> typeShape : desktopPane.typeShapes)
			{
				typeshapeJAXBModels.add(new TypeShape.JAXBModel(typeShape));
			}
			
//			for (Relation relation : desktopPane.relations)
//			{
//				relationXMLModels.add(new RelationXMLModel(relation));
//			}
		}
		
		void afterUnmarshal(Unmarshaller unmarshaller, Object parent)
		{
			LOGGER.debug("parent: " + parent.getClass().getName());
		}
	}
	
	/**
	 * If instance was created from {@link DesktopPane#DesktopPane(JAXBModel)} the
	 * injected parameter will be stored in this field. Otherwise it is
	 * initialised lazily via {@link DesktopPane#getJAXBModel()}.
	 * <p>
	 * <b>Important</b>
	 * <p>
	 * In order to be able to store modifications of the desktop pane instance
	 * (like adding typeshapes for example) make sure to always update the JAXB
	 * model.
	 */
	private DesktopPane.JAXBModel jaxbModel;

	private String name;
	
	/**
	 * desktop manager for {@link Typeshape} instances contained by this pane
	 */
	private DesktopManager desktopManager;

	/**
	 * {@link Typeshape} instances contained by this pane
	 */
	private Set<TypeShape<?>> typeShapes = new HashSet<TypeShape<?>>();

	/**
	 * {@link Relation} instances contained by this pane
	 */
	private Set<Relation> relations = new HashSet<Relation>();
	
	/**
	 * global type shape display options for {@link #typeShapes} in this container
	 */
	private DisplayOptionsModelContainer displayOptionsModelContainer =
			new DisplayOptionsModelContainer();

	/**
	 * Constructs an empty pane and replaces the {@link DesktopManager} of the
	 * super class with {@link #desktopManager}.
	 */
	private DesktopPane()
	{
		super();

//		setBackground(Color.GRAY);
		setOpaque(false);
		
		name = "unnamed desktop pane [" + System.currentTimeMillis() + "]";
		desktopManager = new DesktopManager(this);

		setDesktopManager(desktopManager);

		addMouseListener
		(
				new MouseAdapter()
				{
					public void mouseClicked(MouseEvent evt)
					{
						DesktopPane.this.mouseClicked(evt);
					}
				}
		);
	}
	
	/**
	 * @param name
	 */
	public DesktopPane(String name)
	{
		this();
		this.name = name;
	}

	/**
	 * This constructor is typically used when a desktop pane is to be
	 * reconstructed from a JAXB model.
	 *
	 * @param jaxbModel
	 */
	public DesktopPane(DesktopPane.JAXBModel jaxbModel)
  {
		this(jaxbModel.name);

		// double link instance with jaxbModel
		this.jaxbModel = jaxbModel;
		jaxbModel.desktopPane = this;

		displayOptionsModelContainer = jaxbModel.displayOptionsModelContainer;
		
		for (TypeShape.JAXBModel typeshapeJAXBModel : jaxbModel.typeshapeJAXBModels)
		{
			TypeElement typeElement =
					TypeElementDictionarySingleton
							.instance()
					    .get(typeshapeJAXBModel.typeElementAsString);
			
			if (typeElement == null)
			{
				LOGGER.warn(
						"failure fetching type element from dictionary for " +
							typeshapeJAXBModel.typeElementAsString);
			}
			else
			{
				TypeShape<TypeElement> typeShape =
						new TypeShape<TypeElement>(
								typeElement, displayOptionsModelContainer, typeshapeJAXBModel);
				add(typeShape);
			}
		}

		// finally make the typeshapes visible
		for (TypeShape<?> typeShape : typeShapes)
		{
			typeShape.setVisible(true);
		}

//		// This constructor uses the same methods to add typeshapes to the desktop
//		// pane as would be when a user manually adds a typeshape to the desktop.
//		// Namely the add method inside the loop synchronizes the typeshapes in the
//		// desktop pane model with the typeshapes on the desktop pane. To avoid
//		// duplicate entries in the model a snapshot of the typeshapes in the model
//		// is made and the typeshape collection in the model is cleared afterwards.
//
//		// Create a snapshot from the typeshape collection and iterate it instead of
//		// the original typeshapes because the original typeshape collection will
//		// be modified (rebuilt in fact) inside the loop starting below.
//		Set<TypeShape.JAXBModel> snapshot =
//				new HashSet<TypeShape.JAXBModel>(jaxbModel.typeshapeJAXBModels);
//
//		// Typeshape collection will be rebuilt inside the loop.
//		jaxbModel.typeshapeJAXBModels.clear();
//
//		Iterator<TypeShape.JAXBModel> iterator = snapshot.iterator();
//
//		while (iterator.hasNext())
//		// for some extremely strange reason the variable declaration of
//		// typeShapeModel is not recognized when using a for loop
////		for (TypeShape.JAXBModel typeShapeModel : snapshot);
//		{
//			TypeShape.JAXBModel typeShapeModel = iterator.next();
//			
//			TypeElement typeElement =
//					TypeElementDictionarySingleton.instance()
//					                              .get(typeShapeModel.typeElementAsString);
//			
//			if (typeElement == null)
//			{
//				LOGGER.warn(
//						"failure fetching type element from dictionary for " +
//						typeShapeModel.typeElementAsString);
//			}
//			else
//			{
//				TypeShape<TypeElement> typeShape =
//						new TypeShape<TypeElement>(
//								typeElement, displayOptionsModelContainer, typeShapeModel);
//				add(typeShape);
//			}
//		}
//
//		// finally make the typeshapes visible
//		for (TypeShape<?> typeShape : typeShapes)
//		{
//			typeShape.setVisible(true);
//		}
  }

	protected void mouseClicked(MouseEvent evt)
  {
    if (evt.getButton() != MouseEvent.BUTTON3)
    {
      return;
    }

    Object[] options =
    {
        "capture screen",
        "configure display options"
    };

    int selection =
        JOptionPane.showOptionDialog
        (
            this,
            "",
            "",
            JOptionPane.YES_NO_CANCEL_OPTION,
            JOptionPane.QUESTION_MESSAGE,
            null,
            options,
            options[0]);

    if (selection == 0)
    {
      captureScreen();
      evt.consume();
      return;
    }

		JFrame jFrame = new JFrame();
		
		DisplayOptions displayOptions =
				new DisplayOptions(
						jFrame, true, displayOptionsModelContainer);
		
		displayOptions.setLocation(evt.getLocationOnScreen());
		displayOptions.setVisible(true);
		
		displayOptionsModelContainer.acknowledged = false;
		
		evt.consume();
  }

	/**
	 * Adds <code>typeShape</code> to the desptop pane. Relations to other
	 * typeshapes that may be on the desktop pane already will be calculated
	 * and displayed also.
	 *
	 * @param typeShape
	 * @return
	 */
	public Component add(TypeShape<?> typeShape)
	{
  	if (typeShapes.add(typeShape))
  	{
  		relations.addAll(computeNewRelations(typeShape));
  	}

		return super.add(typeShape);
	}
	
	public Set<TypeShape<?>> getTypeShapes()
	{
		return Collections.unmodifiableSet(typeShapes);
	}

	/**
	 * Adds {@link Typeshape} components to this pane and computes if new
	 * {@link Relation} instances are to be displayed, too.
	 *
   * @see java.awt.Container#add(java.awt.Component)
   * @throws IllegalArgumentException if component is not a {@link Typeshape}
   */
  @Override
  public Component add(Component component) throws IllegalArgumentException
  {
  	if ((component instanceof TypeShape) == false)
  	{
  		throw new IllegalArgumentException(
  				"only " + TypeShape.class.getName() + " components allowed");
  	}

  	return add((TypeShape<?>) component);
  }

	@Override
  public void paint(Graphics graphics)
  {
	  super.paint(graphics);

	  Graphics2D graphics2d = (Graphics2D) graphics;

	  for (Relation relation : relations)
	  {
	  	relation.paint(graphics2d);
	  }
  }
	
	private Set<Relation> computeNewRelations(TypeShape<?> newTypeShape)
	{
		Set<Relation> newRelations = new HashSet<Relation>();

		// compute new relations between type shapes already on pane and new type
		// shape
		for (TypeShape<?> typeShape : typeShapes)
		{
			if (typeShape != newTypeShape)
			{
				Relation relation =
						computeIsARelation(typeShape, newTypeShape);
				
				if (relation != null)
				{
					newRelations.add(relation);
				}

				// consider relations from typeShape to newTypeShape and vice versa
				newRelations.addAll(computeHasRelations(typeShape, newTypeShape));
				newRelations.addAll(computeHasRelations(newTypeShape, typeShape));
			}
		}
		
		// compute relations of new type shape to itself
		newRelations.addAll(computeHasRelations(newTypeShape, newTypeShape));

		return newRelations;
	}

	private Relation computeIsARelation(
			TypeShape<?> typeShapeAlreadyOnPane, TypeShape<?> typeShapeNewOnPane)
	{
		TypeMirror typeMirrorOfTypeAlreadyOnPane =
				typeShapeAlreadyOnPane.getTypeElement().asType();
		TypeMirror typeMirrorOfTypeNewOnPane =
				typeShapeNewOnPane.getTypeElement().asType();
		
		if (ProcessingUtilities.isSubtype(
		    typeMirrorOfTypeAlreadyOnPane, typeMirrorOfTypeNewOnPane))
		{
			return
					new RelationIsA(
							typeShapeAlreadyOnPane, typeShapeNewOnPane, this);
		}
		else if (ProcessingUtilities.isSubtype(
				typeMirrorOfTypeNewOnPane, typeMirrorOfTypeAlreadyOnPane))
		{
			return
					new RelationIsA(
							typeShapeNewOnPane, typeShapeAlreadyOnPane, this);
		}

		return null;
	}

	private Set<Relation> computeHasRelations(
			TypeShape<?> typeShape1, TypeShape<?> typeShape2)
	{
		Set<Relation> result = new HashSet<Relation>();
		
		TypeElement typeElement1 = typeShape1.getTypeElement();
		TypeElement typeElement2 = typeShape2.getTypeElement();
		
		for (RelationInfoHas relationInfoHas :
		     RelationInfoCalculator.calculateRelationInfoHas(
		    		 typeElement1, typeElement2))
		{
			Element enclosingElementOfSource =
					relationInfoHas.getSource().getEnclosingElement();

			if (enclosingElementOfSource != null)
			{
				if (enclosingElementOfSource instanceof TypeElement)
				{
					if (ProcessingUtilities.isSameType(
					        enclosingElementOfSource.asType(), typeElement1.asType()))
					{
						result.add(
								new RelationHasA(
										typeShape1, typeShape2, this, relationInfoHas.getSource()));
					}
					else if (ProcessingUtilities.isSameType(
					             enclosingElementOfSource.asType(),
                       typeElement2.asType()))
					{
						result.add(
								new RelationHasA(
										typeShape2, typeShape1, this, relationInfoHas.getSource()));
					}
					else
					{
						throw new IllegalStateException(
								"source type of relation info does" +
								"neither match type in shape1 nor shape2");
					}
				}
			}
		}
		
		return result;
	}

	protected void captureScreen()
	{
		try
		{
			Dimension size = getSize();
			
			BufferedImage bufferedImage =
			    new BufferedImage(
			    		size.width, size.height, BufferedImage.TYPE_INT_RGB);
			
			paint(bufferedImage.getGraphics());

			ImageIO.write(bufferedImage, "jpg", new File("./screenshot.jpg"));
		}
		catch (IOException e)
		{
			System.err.println("failure capturing screen\n" + e);
		}
	}

	/**
	 * Sets all component size properties ( maximum, minimum, preferred)
	 * to the given dimension.
	 */
	public void setAllSize(Dimension d)
	{
		setMinimumSize(d);
		setMaximumSize(d);
		setPreferredSize(d);
	}

	/**
	 * Sets all component size properties (maximum, minimum, preferred)
	 * to the given width and height.
	 */
	public void setAllSize(int width, int height)
	{
		setAllSize(new Dimension(width, height));
	}
	
	/**
	 * Updates objects depending on <code>typeShape</code>.
	 * 
	 * @param typeShape
	 */
	public void update(TypeShape<?> typeShape)
	{
		typeShape.update();
	}

	@Override
	public JAXBModel getJAXBModel()
	{
		if (jaxbModel == null)
		{
			jaxbModel = new JAXBModel(this);
		}

		return jaxbModel;
	}

	public static DesktopPane createDefaultDesktopPane()
  {
	  return new DesktopPane(DEFAULT_NAME);
  }
}